// Copyright 2013 Petar Maymounkov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ns

import (
	"fmt"
	"log"
	"reflect"
	"sync"
	"time"
	"unicode"
	"unsafe"

	"circuit/kit/rh"
)

// interfaceProc
type interfaceProc struct {
	//
	Name  string
	FName string
	qid   rh.Q
	//
	dir   bool
	walk  func() rh.FID
	mtime time.Time
	//
	__      sync.Mutex
	__ncall int64
	__atime time.Time
}

func newInterfaceProc(i int, t reflect.Type, recv reflect.Value, now time.Time) *interfaceProc {
	m := t.Method(i)
	if !unicode.IsUpper(rune(m.Name[0])) {
		// Not public
		return nil
	}
	p := &interfaceProc{
		Name:  m.Name,
		FName: filename(m.Name),
		qid: rh.Q{
			ID:  valueHash(recv) ^ valueHash(reflect.ValueOf(m.Name)),
			Ver: 1,
		},
		mtime:   now,
		__atime: now,
	}
	mt := m.Type
	mv := recv.Method(i)
	//
	//println("▲", m.Name, mt.NumIn(), mt.NumOut(), fmt.Sprintf("%v", mt))
	if !p.initDir(mt, mv) {
		p.initFile(t.String(), mt, mv)
	}
	return p
}

func (p *interfaceProc) initDir(mt reflect.Type, mv reflect.Value) bool {
	if mt.NumIn() != 1 || mt.NumOut() != 1 {
		return false
	}
	rt := mt.Out(0)
	switch {
	case rt.Implements(typeOfSliceDir):
		p.walk, p.dir = func() rh.FID {
			return newSliceFID(p.FName, mv.Call(nil)[0].Interface().(SliceDir))
		}, true

	case rt.Implements(typeOfMapDir):
		p.walk, p.dir = func() rh.FID {
			return newMapFID(p.FName, mv.Call(nil)[0].Interface().(MapDir))
		}, true

	case rt.Implements(typeOfFID):
		p.walk, p.dir = func() rh.FID {
			r := mv.Call(nil)[0].Interface()
			if r == nil {
				return nil
			}
			return r.(rh.FID)
		}, true

	case rt.Implements(typeOfDir):
		//
		open := func() rh.FID { // open the directory returned by this method
			nsdir, ok := mv.Call(nil)[0].Interface().(Dir)
			if !ok {
				return nil
			}
			return NewFID(p.FName, nsdir)
		}
		stat, _ := statHidden(p.FName, open(), p.qid)
		p.walk, p.dir = func() rh.FID { // walk _to_ the directory returned by this method
			return newHiddenFID(p.FName, open, stat)
		}, true

	default:
		return false

	}
	return true
}

func statHidden(name string, fid rh.FID, q rh.Q) (*rh.Dir, error) {
	var length int64
	var hidden bool
	var aux rh.DirAux
	if fid != nil {
		u, err := fid.Stat()
		if err != nil {
			return nil, err
		}
		length = u.Length
		hidden = u.Mode.IsHidden
		aux = u.Aux
	}
	now := time.Now()
	return &rh.Dir{
		Q: q,
		Mode: rh.Mode{
			Attr:     rh.ModeDir,
			IsHidden: fid == nil || hidden,
		},
		Perm:   0777, // rwxrwxrwx
		Atime:  now,
		Mtime:  now,
		Name:   name,
		Length: length,
		Uid:    uid(),
		Gid:    gid(),
		Aux:    aux,
	}, nil
}

func (p *interfaceProc) initFile(rname string, mt reflect.Type, mv reflect.Value) {
	if mt.NumIn() != 3 || mt.NumOut() != 0 {
		log.Panicf("receiver=%s method=%s", rname, p.Name)
	}
	if !mt.In(1).AssignableTo(typeOfRequestReader) || !mt.In(2).AssignableTo(typeOfResponseWriter) {
		panic("x")
	}
	p.walk = func() rh.FID {
		return newCallFID(p, mv)
	}
}

func (p *interfaceProc) Q() rh.Q {
	return p.qid
}

func (p *interfaceProc) Stat() (*rh.Dir, error) {
	p.__.Lock()
	defer p.__.Unlock()
	if p.dir {
		return p.statdir(p.walk())
	}
	return p.statfile(p.walk())
}

func (p *interfaceProc) statdir(fid rh.FID) (*rh.Dir, error) {
	var length int64
	var hidden bool
	var aux rh.DirAux
	if fid != nil {
		u, err := fid.Stat()
		if err != nil {
			return nil, err
		}
		length = u.Length
		hidden = u.Mode.IsHidden
		aux = u.Aux
	}
	return &rh.Dir{
		Q: p.Q(),
		Mode: rh.Mode{
			Attr:     rh.ModeDir,
			IsHidden: fid == nil || hidden,
		},
		Perm:   0777, // rwxrwxrwx
		Atime:  p.__atime,
		Mtime:  p.mtime,
		Name:   p.FName,
		Length: length,
		Uid:    uid(),
		Gid:    gid(),
		Aux:    aux,
	}, nil
}

func (p *interfaceProc) statfile(fid rh.FID) (*rh.Dir, error) {
	return &rh.Dir{
		Q:      p.Q(),
		Mode:   rh.Mode{Attr: rh.ModeIO},
		Perm:   0666, // rw-rw-rw-
		Atime:  p.__atime,
		Mtime:  p.mtime,
		Name:   p.FName,
		Length: 0, //p.__ncall,
		Uid:    uid(),
		Gid:    gid(),
	}, nil
}

func (p *interfaceProc) hit() {
	p.__.Lock()
	defer p.__.Unlock()
	p.__atime = time.Now()
	p.__ncall++
}

// hiddenFID represents an unopened directory corresponding to a method
// returning ns.Dir. It resolves a friction whereby the user should be
// able to cd into such a directory (even if empty). On open, the directory
// contents is materialized, by calling the underlying method.
type hiddenFID struct {
	rh.ReadOnlyFID
	//
	name   string
	open   func() rh.FID
	z_     sync.Mutex
	z_fid  rh.FID
	z_stat *rh.Dir
}

func newHiddenFID(name string, open func() rh.FID, stat *rh.Dir) *hiddenFID {
	return &hiddenFID{
		name:   name,
		open:   open,
		z_stat: stat,
	}
}

func (h *hiddenFID) String() string {
	h.z_.Lock()
	fid := h.z_fid
	h.z_.Unlock()
	if fid != nil {
		return fmt.Sprintf("hiddenFID▒ %s:(%02x)", h.name, uint64(uintptr(unsafe.Pointer(h)))&0xff)
	}
	return fmt.Sprintf("hiddenFID▒ %s:(··)", h.name)
}

func (h *hiddenFID) Q() rh.Q {
	h.z_.Lock()
	defer h.z_.Unlock()
	return h.z_stat.Q
}

func (h *hiddenFID) Open(rh.Flag, rh.Intr) (err error) {
	h.z_.Lock()
	defer h.z_.Unlock()
	h.z_fid = h.open()
	return nil
}

func (h *hiddenFID) Clunk() {
	h.z_.Lock()
	defer h.z_.Unlock()
	if h.z_fid != nil {
		h.z_fid.Clunk()
	}
	h.z_fid = nil
}

// Stat calls the underlying method to retrieve the current state of the directory, if non-nil.
func (h *hiddenFID) Stat() (*rh.Dir, error) {
	h.Open(rh.Flag{}, nil) // Refresh the dircetory contents
	h.z_.Lock()
	fid := h.z_fid
	h.z_.Unlock()
	if fid == nil {
		h.z_.Lock()
		defer h.z_.Unlock()
		return h.z_stat, nil
	}
	return fid.Stat()
}

func (h *hiddenFID) Walk(wname []string) (rh.FID, error) {
	h.Open(rh.Flag{}, nil) // Refresh the dircetory contents
	h.z_.Lock()
	fid := h.z_fid
	h.z_.Unlock()
	if fid == nil {
		return nil, rh.ErrNotExist // Directory is empty
	}
	return fid.Walk(wname)
}

func (h *hiddenFID) Read(offset int64, count int, intr rh.Intr) (rh.Chunk, error) {
	h.Open(rh.Flag{}, nil) // Refresh the dircetory contents
	h.z_.Lock()
	fid := h.z_fid
	h.z_.Unlock()
	if fid == nil {
		return nil, nil // Still empty
	}
	return fid.Read(offset, count, intr)
}
